package com.beidasoft.web.util.elasticSearch;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @author fuchao
 * @Description TODO
 * @createTime 2023年01月12日 10:31:00
 */
@Service
@Configuration
@EnableScheduling
@Slf4j
public class ElasticSearchUtils {

    private ElasticsearchRestTemplate template;

    @Autowired(required = false)
    public void setElasticsearchRestTemplate(ElasticsearchRestTemplate elasticsearchRestTemplate) {
        this.template = elasticsearchRestTemplate;
    }

    private ObjectMapper objectMapper;

    @PostConstruct
    public void init() {
        JavaTimeModule dateModule = new JavaTimeModule();
        dateModule.addSerializer(LocalDate.class, new LocalDateSerializer());
        dateModule.addDeserializer(LocalDate.class, new LocalDateDeserializer());
        JavaTimeModule timeModule = new JavaTimeModule();
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
        // 设置NULL值不参与序列化
        objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL).registerModules(dateModule, timeModule);
    }

    /**
     * 根据ID判断文档是否存在
     *
     * @param indexName 索引名称
     * @param docId     文档ID
     * @return 存在与否
     */
    public boolean existDocById(String indexName, String docId) {
        if (existsIndex(indexName) && StringUtils.isNotEmpty(docId)) {
            return template.exists(docId, IndexCoordinates.of(indexName));
        }
        return Boolean.FALSE;
    }

    public <T> boolean createIndex(T elasticsearchModel) {
        Class<?> clazz = elasticsearchModel.getClass();
        //判断是否有指定主解
        org.springframework.data.elasticsearch.annotations.Document anno = clazz.getAnnotation(org.springframework.data.elasticsearch.annotations.Document.class);
        if (anno != null) {
            //将注解中的类型值作为key，对应的类作为 value
            String indexName = anno.indexName();
            if(!existsIndex(indexName)) {
                boolean flag = template.indexOps(elasticsearchModel.getClass())
                        .create();
                template.indexOps(elasticsearchModel.getClass())
                        .putMapping(template.indexOps(elasticsearchModel.getClass())
                                .createMapping());
                return flag;
            }
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * 判断索引是否存在
     * @return
     */
    public boolean existsIndex(String indexName) {
        if(StringUtils.isBlank(indexName)) {
            return Boolean.FALSE;
        }
        return template.indexOps(IndexCoordinates.of(indexName)).exists();
    }

    /**
     * 新增文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @return 文档ID
     */
    public <T> String indexDoc(String indexName, T elasticsearchModel) {
        if (existsIndex(indexName)) {
            return template.index(new IndexQueryBuilder().withId(getDocumentIdValue(elasticsearchModel))
                    .withObject(elasticsearchModel).build(), IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 更新文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id标注的文档ID值不能为空
     * @return UpdateResponse.Result
     * @throws JsonProcessingException
     */
    public <T> UpdateResponse.Result updateDoc(String indexName, T elasticsearchModel) throws JsonProcessingException {
        return updateDoc(indexName, elasticsearchModel, this.objectMapper);
    }

    /**
     * 更新文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id标注的文档ID值不能为空
     * @param objectMapper       objectMapper
     * @return UpdateResponse.Result
     * @throws JsonProcessingException
     */
    public <T> UpdateResponse.Result updateDoc(String indexName, T elasticsearchModel, ObjectMapper objectMapper) throws JsonProcessingException {
        if (StringUtils.isNotEmpty(indexName) && elasticsearchModel != null) {
            Assert.isTrue(existDocById(indexName, getDocumentIdValue(elasticsearchModel)), "elasticsearch document miss.");
            String json = objectMapper.writeValueAsString(elasticsearchModel);
            UpdateQuery updateQuery = UpdateQuery.builder(getDocumentIdValue(elasticsearchModel)).withDocument(Document.parse(json)).build();
            return template.update(updateQuery, IndexCoordinates.of(indexName)).getResult();
        }
        return UpdateResponse.Result.NOOP;
    }

    /**
     * 删除文档
     *
     * @param indexName   索引名称
     * @param docId       文档主键
     * @return boolean
     * @throws
     */
    public boolean deleteDoc(String indexName, String docId) {
        if (existsIndex(indexName)) {
            String temp = template.delete(docId, IndexCoordinates.of(indexName));
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    @NonNull
    private <T> String getDocumentIdValue(T elasticsearchModel) {
        validDocument(elasticsearchModel);
        List<Field> fields = ReflectUtils.getClassFieldsByAnnotation(elasticsearchModel.getClass(), Id.class);
        // notEmpty 已校验notNull, 但是编译器无法检测NPE; 添加此句抑制编译器
        Assert.notNull(fields, elasticsearchModel.getClass().getSimpleName()
                + " no fields marked with @" + Id.class.getName() + " annotation.");
        Assert.notEmpty(fields, elasticsearchModel.getClass().getSimpleName()
                + " no fields marked with @" + Id.class.getName() + " annotation.");
        Object fieldValue = ReflectUtils.getFieldValue(elasticsearchModel, fields.get(0));
        Assert.isTrue(fieldValue != null && StringUtils.isNotEmpty(fieldValue.toString()),
                elasticsearchModel.getClass().getSimpleName() + " @Id value must not be null.");
        return String.valueOf(fieldValue);
    }

    /**
     * 校验JavaBean是否实现了@Document注解
     *
     * @param elasticsearchModel elasticsearch bean
     * @param <T>
     */
    private <T> void validDocument(T elasticsearchModel) {
        Assert.notNull(elasticsearchModel, elasticsearchModel.getClass().getSimpleName() + " must not be null.");
        validDocument(elasticsearchModel.getClass());
    }

    private <T> void validDocument(Class<T> tClass) {
        Assert.notNull(tClass, tClass.getSimpleName() + " must not be null.");
        org.springframework.data.elasticsearch.annotations.Document document = tClass
                .getAnnotation(org.springframework.data.elasticsearch.annotations.Document.class);
        Assert.notNull(document, tClass.getSimpleName() + " must have @"
                + org.springframework.data.elasticsearch.annotations.Document.class.getName() + " annotation.");
    }

    /**
     * 查询文档
     *
     * @param indexName    索引名称
     * @param tClass       映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder 非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, QueryBuilder queryBuilder) {
        return search(indexName, tClass, queryBuilder, null, null, null, null);
    }

    /**
     * 查询文档
     *
     * @param indexName    索引名称
     * @param tClass       映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder 非结构化数据 QueryBuilder
     * @param pageable     FilterQueryBuilder
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, QueryBuilder queryBuilder, Pageable pageable) {
        return search(indexName, tClass, queryBuilder, null, null, pageable, null);
    }

    /**
     * 查询文档
     * 查询QueryBuilder默认包含existQuery(tClass.@Id标注的字段)
     * <p>
     * 查询的文档必须包含映射@Document的@Id字段（父子文档关系索引中, 因父子文档保存在一个索引中,
     * 而JavaBean对应父子文档是分开的，业务查询的时候不希望查询到无关业务属性的文档数据映射出来，
     * 故通过包含单个父/子文档的@Id字段避免无关数据问题）
     * </p>
     *
     * @param indexName                  索引名称
     * @param tClass                     映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder               非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param filterBuilder              结构化数据 QueryBuilder; filterBuilder与queryBuilder必须二者存在其一
     * @param abstractAggregationBuilder 聚合查询Builder
     * @param pageable                   分页/排序; 分页从0开始
     * @param fields                     包含字段
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, @Nullable QueryBuilder queryBuilder,
                                    @Nullable QueryBuilder filterBuilder, @Nullable AbstractAggregationBuilder abstractAggregationBuilder,
                                    @Nullable Pageable pageable, @Nullable String[] fields) {
        if (existsIndex(indexName)) {
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder().withQuery(queryBuilder);
            nativeSearchQueryBuilder.withTrackScores(true);
            if (filterBuilder != null) {
                nativeSearchQueryBuilder.withFilter(filterBuilder);
            }
            if (abstractAggregationBuilder != null) {
                nativeSearchQueryBuilder.addAggregation(abstractAggregationBuilder);
                nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());
            }
            if (pageable != null) {
                nativeSearchQueryBuilder.withPageable(pageable);
            }
            if (fields != null && fields.length > 0) {
                nativeSearchQueryBuilder.withFields(fields);
            }
            //解除查询只有10000条限制
            NativeSearchQuery build = nativeSearchQueryBuilder.build();
            build.setTrackTotalHits(true);
            return template.search(build, tClass, IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 根据ID查询文档
     *
     * @param indexName 索引名称
     * @param docId     文档ID
     * @param tClass    映射类Class
     * @param <T>
     * @return Elasticsearch 文档
     */
    public <T> T findById(String indexName, String docId, Class<T> tClass) {
        if (existsIndex(indexName) && StringUtils.isNotEmpty(docId) && tClass != null) {
            return template.get(docId, tClass, IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 将es检索返回Hits转换为List
     *
     * @param searchHits es检索对象
     * @param clazz      文档实体类
     * @param <T>
     * @return
     */
    public static  <T> List<T> hitsToList(SearchHits<T> searchHits, Class<T> clazz) {
        List<T> targets = new ArrayList<>();
        for (SearchHit hit : searchHits) {
            Map<String, List<String>> hitMap = hit.getHighlightFields();
            Iterator<String> keys = hitMap.keySet().iterator();
            JSONObject object = JSONObject.parseObject(JSON.toJSONString(hit.getContent()));

            while (keys.hasNext()) {
                String key = keys.next();
                object.put(key, hitMap.get(key).get(0));
            }

            targets.add(JSONObject.parseObject(object.toJSONString(), clazz));
        }
        return targets;
    }
}
